#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>       
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>

#include "bloom_filter.h"
#include "util.h"

#define TYPECAST_LBF_PORT 2004
#define MAX_BUFFER_SIZE 1024

static char line_buf[2048]; 
static char read_buf[2048]; 
static int reread_buf = 0; 

static FILE * config_file = NULL;

typedef struct struct_server_statistics_node
{
    u_int32_t iSessionID;
    u_int32_t srcAddr;
    bloom_filter_t src_bloom_filter;
    int count;
    struct struct_server_statistics_node* next;
} server_statistics_node;

typedef struct struct_server_node
{
    int sock_fd;
    u_int32_t dstAddr;
    int dstPort;
    bloom_filter_t dst_bloom_filter;
    struct struct_server_statistics_node* pStatistics;
    struct struct_server_node* next;
} server_node;

server_node* g_pServerNodesHead = NULL;
server_node* g_pServerNodesTail = NULL;

typedef struct struct_session_count_node
{
    u_int32_t iSessionID;
    int currentCount;
    int lastCount;
    int iDone; // 1 = done
    struct struct_session_count_node* next;
} session_count_node;

session_count_node* g_pSessionCountNodeHead = NULL;
session_count_node* g_pSessionCountNodeTail = NULL;


char * read_line() 
{ 
    int len; 
    if (reread_buf) { 
        reread_buf = 0; 
        strcpy(line_buf, read_buf); 
        return line_buf; 
    }

    while(fgets(read_buf, sizeof(read_buf), config_file) != NULL) { 
        if (read_buf[0] == '\0' || read_buf[0] == '#' || read_buf[0] == ' ' || read_buf[0] == '\t' || read_buf[0] == '\n') { 
            continue; 
        }
        len = strlen(read_buf); 
        if (read_buf[len-1] == '\n') { 
            read_buf[len-1] = '\0'; 
        }

        strcpy(line_buf, read_buf); 
        return line_buf; 
    }
    return NULL; 
}

void unread_line() 
{
    reread_buf = 1; 
} 

// 0 on success, -1 on failure
int parse_entries()
{
    /*
        address: xxx.xxx.xxx.xxx
        port: xxxx
        bloomfilter: xxxxxxxxxxxxxxxx

        ...
    */

    char * line;
    while ((line = read_line()) != NULL)
    {
        // First line has to be address: xxx.xxx.xxx.xxx
        if (strstr(line, "address: ") != NULL)
        {
            server_node * pServerNode = malloc(sizeof(server_node));

            pServerNode->dstAddr = inet_addr(line + sizeof("address:"));
            line = read_line();

            if (strstr(line, "port: ") == NULL)
                return -1;
            pServerNode->dstPort = atoi(line + sizeof("port:"));
            line = read_line();

            if (strstr(line, "bloomfilter: ") == NULL)
                return -1;
            hex_string_to_bloom_filter(line + sizeof("bloomfilter:"), pServerNode->dst_bloom_filter);

            pServerNode->next = NULL;
            pServerNode->pStatistics = NULL;
            pServerNode->sock_fd = 0;

            if (g_pServerNodesHead == NULL)
            {
                g_pServerNodesHead = pServerNode;
                g_pServerNodesTail = pServerNode;
            }
            else
            {
                g_pServerNodesTail->next = pServerNode;
                g_pServerNodesTail = pServerNode;
            }

            printf("   Joined %s:%d - BF:%s\n",
                   ip_to_string(htonl(pServerNode->dstAddr)),
                   pServerNode->dstPort,
                   bloom_filter_to_string(pServerNode->dst_bloom_filter));

        }
        else
        {
            break;
        }
    }



    if (g_pServerNodesHead == NULL)
        return -1;

    return 0;
}

void parse_host() 
{
    struct in_addr in; 
    char * line; 
    char *token; 

    u_int32_t local_ip = get_local_ip_address();     
    while((line = read_line()) != NULL) {      
        char * token = strsep(&line, ":=");
        if (strcmp(token, "host") == 0) { 
            // Check whether this host is the equivalent to the local host
            char * addr = line; 
            // skip white spaces
            while( (*addr == ' ') || (*addr == '\t') ) { 
                addr++; 
            }
            if (inet_aton(addr, &in) == 0) { 
                perror("Error when converting network address"); 
                exit(-1); 
            }
            if (ntohl(in.s_addr) != local_ip) { 
                continue; 
            }
            
            if (parse_entries() != 0)
            {
                perror("Error parsing config file");
                exit(-1);
            }

            return; 
        }
    }    
    fprintf(stderr, "No host entry found for this host, check your configuration file\n"); 
    exit(-1); 
}


server_statistics_node* addBloomFilterEntry(server_node* pServer, u_int32_t iSessionID, u_int32_t srcAddr, bloom_filter_t BF)
{
    // Add to session count
    session_count_node* pSessionCount;
    pSessionCount = g_pSessionCountNodeHead;
    while (pSessionCount != NULL)
    {
        if (pSessionCount->iSessionID == iSessionID)
        {
            pSessionCount->currentCount++;
            pSessionCount->iDone = 0;
            break;
        }
        pSessionCount = pSessionCount->next;
    }
    
    if (pSessionCount == NULL)
    {
        pSessionCount = (session_count_node*) malloc(sizeof(session_count_node));
        pSessionCount->iSessionID = iSessionID;
        pSessionCount->currentCount = 1;
        pSessionCount->lastCount = 0;
        pSessionCount->iDone = 0;
        pSessionCount->next = NULL;

        if (g_pSessionCountNodeHead == NULL)
        {
            g_pSessionCountNodeHead = pSessionCount;
            g_pSessionCountNodeTail = pSessionCount;
        }
        else
        {
            g_pSessionCountNodeTail->next = pSessionCount;
            g_pSessionCountNodeTail = pSessionCount;
        }
    }


    // Add it to the statistics node
    server_statistics_node* pStatistics;
    pStatistics = pServer->pStatistics;
    server_statistics_node* pStatisticsPrev = NULL;

    while (pStatistics != NULL)
    {
        if ((pStatistics->iSessionID ==iSessionID) &&
            (pStatistics->srcAddr == srcAddr) &&
            (memcmp(pStatistics->src_bloom_filter, BF, sizeof(bloom_filter_t)) == 0))
        {
            pStatistics->count++;
            return pStatistics;
        }

        pStatisticsPrev = pStatistics;
        pStatistics = pStatistics->next;
    }

    pStatistics = (server_statistics_node*) malloc(sizeof(server_statistics_node));
    pStatistics->iSessionID = iSessionID;
    pStatistics->srcAddr = srcAddr;
    copy_bloom_filter(pStatistics->src_bloom_filter, BF);
    pStatistics->count = 1;
    pStatistics->next = NULL;

    if (pStatisticsPrev == NULL)
    {
        pServer->pStatistics = pStatistics;
    }
    else
    {
        pStatisticsPrev->next = pStatistics;
    }

    return pStatistics;
}

void displayBloomFilters()
{
    server_node* pServer = g_pServerNodesHead;
    server_statistics_node* pStatistics = NULL;

    printf("\nBloomFilters sent to:\n");

    while (pServer != NULL)
    {
        printf("   ip=%s, port=%d, bf=%s\n", ip_to_string(htonl(pServer->dstAddr)), pServer->dstPort, bloom_filter_to_string(pServer->dst_bloom_filter));
        printf("       from:\n");
        pStatistics = pServer->pStatistics;
        while (pStatistics != NULL)
        {
            printf("          sessionid:%d, ip:%s, bf=%s, count=%d\n", 
                pStatistics->iSessionID, ip_to_string(htonl(pStatistics->srcAddr)),
                bloom_filter_to_string(pStatistics->src_bloom_filter), pStatistics->count);

            pStatistics = pStatistics->next;
        }

        // TODO: Add timestamp

        pServer = pServer->next;
    }
}

main(int argc, char **argv) 
{ 
    if (argc < 2)
    {
        printf("Usage: %s <ConfigFile>\n", argv[0]);
        exit(1);
    }

    // Process the config file
    config_file = fopen(argv[1], "r");
    if (config_file == NULL)
    {
        perror("config file not found.\n");
        exit(-1);
    }

    struct timeval start;
    gettimeofday(&start, NULL);
    printf("Server %s begin - %s", ip_to_string(get_local_ip_address()), ctime(&start.tv_sec));

    // Parse configuration file
    parse_host();

    fd_set rfds; 
    fd_set readers;         
    int nfds = 0; 
    FD_ZERO(&readers); 

    server_node* pServer = g_pServerNodesHead;
    while (pServer != NULL)
    {
        pServer->sock_fd = socket(PF_INET, SOCK_DGRAM, 0);
        if (pServer->sock_fd < 0)
        { 
            perror("Error when creating socket"); 
            exit(1); 
        }

        if (MULTICAST(pServer->dstAddr))
        {
            // Update bloom filter here
            struct sockaddr_in dst_addr; 
            memset(&dst_addr, 0, sizeof(struct sockaddr_in));
            dst_addr.sin_family = AF_INET; 
            dst_addr.sin_port = htons(TYPECAST_LBF_PORT); 
            dst_addr.sin_addr.s_addr = htonl(get_local_ip_address()); 
            int sockfd = socket(PF_INET, SOCK_DGRAM, 0);

            char buf[MAX_BUFFER_SIZE];
            memset(buf, 0, MAX_BUFFER_SIZE);

            char * tail = buf;

            *tail = 1;
            tail += sizeof(u_int8_t);

            int i;
            u_int32_t *grp_addr; 

            grp_addr = tail; 
            *grp_addr = (u_int32_t) pServer->dstAddr;
            tail += sizeof(u_int32_t);
            copy_bloom_filter(tail, pServer->dst_bloom_filter);
            tail += sizeof(bloom_filter_t);

            int msglen = sendto(sockfd, buf, tail - buf, 0, (struct sockaddr *) &dst_addr, sizeof(dst_addr) );
            if (msglen < 0)
            {
                perror("Error sendto\n");
            }

            printf("Updated multicast group %s - LBF:%s\n", ip_to_string(ntohl(*grp_addr)), bloom_filter_to_string(pServer->dst_bloom_filter)); 

            // Avoid timing issues - let LBF update.
            sleep(1);

            // Set multicast membership.
            u_char loop = 1;
            if (setsockopt(pServer->sock_fd, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)) < 0)
            {
                perror("Error when setsockopt IP_MULTICAST_LOOP");
                exit(1);
            }

            static struct ip_mreq mreq; 
            mreq.imr_multiaddr.s_addr = pServer->dstAddr; 
            mreq.imr_interface.s_addr = INADDR_ANY; 
            if (setsockopt(pServer->sock_fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
            { 
                perror("Error when setsockopt"); 
                exit(1); 
            }
        }

        struct sockaddr_in serverName;
        struct sockaddr_in clientName;
        memset(&serverName, 0, sizeof(serverName));
        memset(&clientName, 0, sizeof(clientName));

        serverName.sin_family = AF_INET;
        serverName.sin_addr.s_addr = htonl(INADDR_ANY);
        serverName.sin_port = htons(pServer->dstPort);

        if (bind(pServer->sock_fd, (struct sockaddr *) &serverName, sizeof(serverName)) < 0)
        { 
            perror("bind()");
            exit(1);
        }

        FD_SET(pServer->sock_fd, &readers);
        if (pServer->sock_fd >= nfds)
            nfds = pServer->sock_fd + 1;        

        pServer = pServer->next;
    }

    ssize_t msglen; 
    char buf[2048]; 
    struct sockaddr_in src_addr = {0};
    socklen_t addrlen = sizeof(src_addr); 
    bloom_filter_t packet_bloom_filter;
    char string_bloom_filter[BLOOM_FILTER_SIZE * 2 + 1];

    memset(buf, 0, 2048);

    int totalPacketsReceived = 0;
    struct timeval now;
    struct timeval timeOfLastLog;
    gettimeofday(&timeOfLastLog, NULL);

    int fd;
    fd = open( "session_log", O_RDWR | O_CREAT | O_TRUNC, S_IROTH |
        S_IWUSR | S_IRUSR | S_IRGRP );
    close ( fd );

    for (;;)
    {
        memcpy((char *) &rfds, (char *) &readers, sizeof(rfds));        

        struct timeval selectTimeout;
        selectTimeout.tv_sec = 1;
        selectTimeout.tv_usec = 0;
        select(nfds, &rfds, NULL, NULL, &selectTimeout);

        gettimeofday(&now, NULL);

        pServer = g_pServerNodesHead;
        while (pServer != NULL)
        {
            if (FD_ISSET(pServer->sock_fd, &rfds))
            {
                msglen =  recvfrom(pServer->sock_fd, buf, sizeof(buf), 0, &src_addr, &addrlen);
                if (msglen < 0) 
                { 
                    perror("Error when recvfrom"); 
                    continue; 
                }

                // Packet has first the BloomFilter
                char * tail = buf;
                copy_bloom_filter(packet_bloom_filter, tail);
                tail += sizeof(bloom_filter_t);

                // Then the Session ID
                u_int32_t * pSessionID = (u_int32_t *) tail;
                u_int32_t iSessionID = *pSessionID;
                tail += sizeof(u_int32_t);

                // Then the total number of packets to expect this session
                int * pTotalToReceive = (int *) (tail);
                int iTotalToReceive = *pTotalToReceive;
                tail += sizeof(int);

                // And the sequence number of this packet this session
                int * pSequenceNumber = (int *) (tail);
                int iSequenceNumber = *pSequenceNumber;
                tail += sizeof(int);

                // Add this packet to our statistics
                server_statistics_node* curStatistics;
                curStatistics = addBloomFilterEntry(pServer, iSessionID, ntohl(src_addr.sin_addr.s_addr), packet_bloom_filter);

                // Debug purposes
                //displayBloomFilters();

                totalPacketsReceived++;

                printf("Session %d - Received on %s:%d from %s - %d bytes - %d of %d - Total %d received\n", 
                    iSessionID, ip_to_string(htonl(pServer->dstAddr)), pServer->dstPort,
                    ip_to_string(ntohl(src_addr.sin_addr.s_addr)), (int) msglen,
                    iSequenceNumber, iTotalToReceive, curStatistics->count);

/*              // ctime includes trailing endline.
                printf("Session %d - Received on %s:%d from %s - %d bytes - BF:%s - %d of %d - %s", 
                    iSessionID, ip_to_string(htonl(pServer->dstAddr)), pServer->dstPort,
                    ip_to_string(ntohl(src_addr.sin_addr.s_addr)), (int) msglen,
                    bloom_filter_to_string(packet_bloom_filter),
                    curStatistics->count, iTotalToReceive,
                    ctime(&now.tv_sec)); */

                break;
            }

            pServer = pServer->next;
        }

        // If 30 seconds has passed since last entry
        if ((now.tv_sec - timeOfLastLog.tv_sec) > 30)
        {
            // Traverse through all sessions and see if there are any changes
            session_count_node* pSessionCount;
            pSessionCount = g_pSessionCountNodeHead;
            while (pSessionCount != NULL)
            {
                if (pSessionCount->iDone == 0)
                {
                    if (pSessionCount->currentCount == pSessionCount->lastCount)
                    {
                        FILE* log;
                        log = fopen("session_log", "a");
                        if(log == NULL)
                        {
                            perror("error opening session_log");
                        }

                        pSessionCount->iDone = 1;
                        printf("Session ID %d - %d received.\n", 
                            pSessionCount->iSessionID,
                            pSessionCount->currentCount);
                        fprintf(log, "Session ID %d - %d received.\n", 
                            pSessionCount->iSessionID,
                            pSessionCount->currentCount);

                        fclose(log);
                    }

                    pSessionCount->lastCount = pSessionCount->currentCount;
                }

                pSessionCount = pSessionCount->next;
            }

            gettimeofday(&timeOfLastLog, NULL);
        }
    }
}
